class mat4 {
  static final int ROWS = 4;
  static final int COLS = 4;
  static final int ELEMENTS = ROWS * COLS;
  
  static final ZERO = const [0, 0, 0, 0,
                             0, 0, 0, 0,
                             0, 0, 0, 0,
                             0, 0, 0, 0];
  static final IDENTITY = const [1, 0, 0, 0,
                                 0, 1, 0, 0,
                                 0, 0, 1, 0,
                                 0, 0, 0, 1];
  
  final List<num> m;
  
  mat4() : m = new List<num>(ELEMENTS) { }
  mat4.from(mat4 other) : m = new List<num>.from(other.m) { }
  mat4.fromElements(
    num m00, num m01, num m02, num m03,
    num m10, num m11, num m12, num m13,
    num m20, num m21, num m22, num m23,
    num m30, num m31, num m32, num m33) : m = new List<num>.from([m00, m01, m02, m03,
                                                                  m10, m11, m12, m13,
                                                                  m20, m21, m22, m23,
                                                                  m30, m31, m32, m33]) { }
  mat4.zero() : m = new List<num>.from(ZERO) { }
  mat4.identity() : m = new List<num>.from(IDENTITY) { }
  mat4.perspective(num fovY, num aspect, num near, num far) : m = new List<num>(ELEMENTS) {
    perspective(fovY, aspect, near, far);
  }
  mat4.scaling(num x, num y, num z) : m = new List<num>(ELEMENTS) {
    scaling(x, y, z);
  }
  mat4.rotationX(num angle) : m = new List<num>(ELEMENTS) {
    rotationX(angle);
  }
  mat4.rotationY(num angle) : m = new List<num>(ELEMENTS) {
    rotationY(angle);
  }
  mat4.rotationZ(num angle) : m = new List<num>(ELEMENTS) {
    rotationZ(angle);
  }
  mat4.translation(num x, num y, num z) : m = new List<num>(ELEMENTS) {
    translation(x, y, z);
  }
  mat4._fromList(this.m);
  
  void perspective(num fovY, num aspect, num near, num far) {
    num y = 1.0 / Math.tan(fovY / 2);
    num x = y / aspect;
    
    num a = far / (near - far);
    num b = (far * near) / (near - far);
    
    m[ 0] = x; m[ 1] = 0; m[ 2] = 0; m[ 3] = 0;
    m[ 4] = 0; m[ 5] = y; m[ 6] = 0; m[ 7] = 0;
    m[ 8] = 0; m[ 9] = 0; m[10] = a; m[11] = -1;
    m[12] = 0; m[13] = 0; m[14] = b; m[15] = 0;
  }
  
  void scaling(num x, num y, num z) {
    m[ 0] = x; m[ 1] = 0; m[ 2] = 0; m[ 3] = 0;
    m[ 4] = 0; m[ 5] = y; m[ 6] = 0; m[ 7] = 0;
    m[ 8] = 0; m[ 9] = 0; m[10] = z; m[11] = 0;
    m[12] = 0; m[13] = 0; m[14] = 0; m[15] = 1;
  }
  
  void rotationX(num angle) {
    double s = Math.sin(angle);
    double c = Math.cos(angle);    
    m[ 0] = 1; m[ 1] =  0; m[ 2] = 0; m[ 3] = 0;
    m[ 4] = 0; m[ 5] =  c; m[ 6] = s; m[ 7] = 0;
    m[ 8] = 0; m[ 9] = -s; m[10] = c; m[11] = 0;
    m[12] = 0; m[13] =  0; m[14] = 0; m[15] = 1;
  }
  
  void rotationY(num angle) {
    double s = Math.sin(angle);
    double c = Math.cos(angle);    
    m[ 0] = c; m[ 1] = 0; m[ 2] = -s; m[ 3] = 0;
    m[ 4] = 0; m[ 5] = 1; m[ 6] =  0; m[ 7] = 0;
    m[ 8] = s; m[ 9] = 0; m[10] =  c; m[11] = 0;
    m[12] = 0; m[13] = 0; m[14] =  0; m[15] = 1;
  }
  
  void rotationZ(num angle) {
    double s = Math.sin(angle);
    double c = Math.cos(angle);    
    m[ 0] =  c; m[ 1] = s; m[ 2] = 0; m[ 3] = 0;
    m[ 4] = -s; m[ 5] = c; m[ 6] = 0; m[ 7] = 0;
    m[ 8] =  0; m[ 9] = 0; m[10] = 1; m[11] = 0;
    m[12] =  0; m[13] = 0; m[14] = 0; m[15] = 1;
  }
  
  void translation(num x, num y, num z) {
    m[ 0] = 1; m[ 1] = 0; m[ 2] = 0; m[ 3] = 0;
    m[ 4] = 0; m[ 5] = 1; m[ 6] = 0; m[ 7] = 0;
    m[ 8] = 0; m[ 9] = 0; m[10] = 1; m[11] = 0;
    m[12] = x; m[13] = y; m[14] = z; m[15] = 1;
  }
  
  void translate2D(num x, num y) {
    m[12] += (m[0] * x) + (m[4] * y);
    m[13] += (m[1] * x) + (m[5] * y);
    m[14] += (m[2] * x) + (m[6] * y);
    m[15] += (m[3] * x) + (m[7] * y);
  }
  
  void translate3D(num x, num y, num z) {
    m[12] += (m[0] * x) + (m[4] * y) + (m[ 8] * z);
    m[13] += (m[1] * x) + (m[5] * y) + (m[ 9] * z);
    m[14] += (m[2] * x) + (m[6] * y) + (m[10] * z);
    m[15] += (m[3] * x) + (m[7] * y) + (m[11] * z);
  }
  
  mat4 operator negate() {
    return new mat4._fromList([-m[ 0], -m[ 1], -m[ 2], -m[ 3],
                               -m[ 4], -m[ 5], -m[ 6], -m[ 7],
                               -m[ 8], -m[ 9], -m[10], -m[11],
                               -m[12], -m[13], -m[14], -m[15]]);
  }
  
  mat4 operator +(mat4 other) {
    final a = m;
    final b = other.m;
    return new mat4._fromList([a[ 0] + b[ 0], a[ 1] + b[ 1], a[ 2] + b[ 2], a[ 3] + b[ 3],
                               a[ 4] + b[ 4], a[ 5] + b[ 5], a[ 6] + b[ 6], a[ 7] + b[ 7],
                               a[ 8] + b[ 8], a[ 9] + b[ 9], a[10] + b[10], a[11] + b[11],
                               a[12] + b[12], a[13] + b[13], a[14] + b[14], a[15] + b[15]]);
  }
  
  mat4 operator -(mat4 other) {
    final a = m;
    final b = other.m;
    return new mat4._fromList([a[ 0] - b[ 0], a[ 1] - b[ 1], a[ 2] - b[ 2], a[ 3] - b[ 3],
                               a[ 4] - b[ 4], a[ 5] - b[ 5], a[ 6] - b[ 6], a[ 7] - b[ 7],
                               a[ 8] - b[ 8], a[ 9] - b[ 9], a[10] - b[10], a[11] - b[11],
                               a[12] - b[12], a[13] - b[13], a[14] - b[14], a[15] - b[15]]);
  }
  
  mat4 operator *(mat4 other) {
    final a = other.m;
    final b = m;
    return new mat4._fromList([a[ 0] * b[ 0] + a[ 1] * b[ 4] + a[ 2] * b[ 8] + a[ 3] * b[12],
                               a[ 0] * b[ 1] + a[ 1] * b[ 5] + a[ 2] * b[ 9] + a[ 3] * b[13],
                               a[ 0] * b[ 2] + a[ 1] * b[ 6] + a[ 2] * b[10] + a[ 3] * b[14],
                               a[ 0] * b[ 3] + a[ 1] * b[ 7] + a[ 2] * b[11] + a[ 3] * b[15],
                               a[ 4] * b[ 0] + a[ 5] * b[ 4] + a[ 6] * b[ 8] + a[ 7] * b[12],
                               a[ 4] * b[ 1] + a[ 5] * b[ 5] + a[ 6] * b[ 9] + a[ 7] * b[13],
                               a[ 4] * b[ 2] + a[ 5] * b[ 6] + a[ 6] * b[10] + a[ 7] * b[14],
                               a[ 4] * b[ 3] + a[ 5] * b[ 7] + a[ 6] * b[11] + a[ 7] * b[15],
                               a[ 8] * b[ 0] + a[ 9] * b[ 4] + a[10] * b[ 8] + a[11] * b[12],
                               a[ 8] * b[ 1] + a[ 9] * b[ 5] + a[10] * b[ 9] + a[11] * b[13],
                               a[ 8] * b[ 2] + a[ 9] * b[ 6] + a[10] * b[10] + a[11] * b[14],
                               a[ 8] * b[ 3] + a[ 9] * b[ 7] + a[10] * b[11] + a[11] * b[15],
                               a[12] * b[ 0] + a[13] * b[ 4] + a[14] * b[ 8] + a[15] * b[12],
                               a[12] * b[ 1] + a[13] * b[ 5] + a[14] * b[ 9] + a[15] * b[13],
                               a[12] * b[ 2] + a[13] * b[ 6] + a[14] * b[10] + a[15] * b[14],
                               a[12] * b[ 3] + a[13] * b[ 7] + a[14] * b[11] + a[15] * b[15]]);
  }
  
  String toString() => '{(${m[0]},${m[1]},${m[2]},${m[3]}),(${m[4]},${m[5]},${m[6]},${m[7]}),(${m[8]},${m[9]},${m[10]},${m[11]}),(${m[12]},${m[13]},${m[14]},${m[15]})}';
}
